Skip to content

feat(activesync): force Sync TruncationSize override - #96

Open
TDannhauer wants to merge 1 commit into
FRAMEWORK_6_0from
feat/maximum-truncation-size
Open

feat(activesync): force Sync TruncationSize override#96
TDannhauer wants to merge 1 commit into
FRAMEWORK_6_0from
feat/maximum-truncation-size

Conversation

@TDannhauer

@TDannhauer TDannhauer commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add optional $conf['activesync']['sync']['forcetruncationsize'] (default 0 = honor client): when set, the server always uses that byte value for Sync body export, whether the client asked for more or less.
  • ItemOperations Fetch intentionally remains uncapped so clients that re-fetch truncated bodies (Nine, iOS) still get a full message on demand.
  • Still reads deprecated maximumtruncationsize from existing configs until horde/base admin UI is updated.

Motivation

Per Sebastian's feedback and @ralflang's direction: one override knob instead of separate min/max settings. Low values reduce Sync traffic for well-behaved clients; high values can help Gmail (which never re-fetches). See doc/clients.md and the horde/base config description for fleet policy guidance.

Related

Test plan

  • TruncationCapTest (force overrides larger/smaller/unlimited client values; deprecated key fallback)
  • Manual: force 500 on Nine → truncated Sync + full body via ItemOperations button; force 1048576 on Gmail → bodies up to 1M complete on Sync

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

🔍 CI Results

Overall: ❌ 12/12 lanes failed

TL;DR: ❌ Quality issues: PHPStan: 243 unique errors in 11 lanes; PHP-CS-Fixer: 33 files.

Summary by PHP Version

PHP dev stable
8.0
8.1
8.2
8.3
8.4
8.5

Quality Metrics

  • PHPUnit: did not run (11 lanes exited non-zero with no test output) ❌
  • PHPStan (advisory): 243 unique errors in 11 lanes ⚠️
  • PHP-CS-Fixer: 33 unique files with issues (of 214 checked) ⚠️
❌ Failed Lanes

php8.0-dev

  • PHPUnit: 0 failures, 0 errors

php8.0-stable

  • PHPUnit: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)
  • PHPStan: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)

php8.1-dev

  • PHPUnit: 0 failures, 0 errors

php8.1-stable

  • PHPUnit: 0 failures, 0 errors

php8.2-dev

  • PHPUnit: 0 failures, 0 errors

php8.2-stable

  • PHPUnit: 0 failures, 0 errors

php8.3-dev

  • PHPUnit: 0 failures, 0 errors

php8.3-stable

  • PHPUnit: 0 failures, 0 errors

php8.4-dev

  • PHPUnit: 0 failures, 0 errors
  • PHP-CS-Fixer: 33 files with issues

php8.4-stable

  • PHPUnit: 0 failures, 0 errors

php8.5-dev

  • PHPUnit: 0 failures, 0 errors

php8.5-stable

  • PHPUnit: 0 failures, 0 errors

CI powered by horde-componentsView full results

@TDannhauer
TDannhauer marked this pull request as draft July 26, 2026 13:18
@TDannhauer
TDannhauer force-pushed the feat/maximum-truncation-size branch 2 times, most recently from 97e1b7d to 624c4fe Compare July 30, 2026 11:30
@SebastianArcus

Copy link
Copy Markdown

I have just tested this with Nine and a value of 500, and if my reading of the logs is correct, it appears to be ignored. EstimatedDataSize seems to be all over the place - and nowhere hear 500. Also slightly odd I found the following log fragment - which seems like a really large number for the body of an email:

[307][2026-08-05T10:50:16+01:00] O:        <AirSyncBase:EstimatedDataSize>
[307][2026-08-05T10:50:16+01:00] O:         9223372036854775807

I will send over the logs in a minute

@TDannhauer

Copy link
Copy Markdown
Contributor Author

Thanks for the retest with maximumtruncationsize = 500 on Nine — the logs show the cap is working. The confusing part is which field to look at.

EstimatedDataSize is not the truncation ceiling

EstimatedDataSize is the estimate of the full body size. With the server cap applied you correctly get values like 25877 (or larger) together with Truncated=1 and only ~500 bytes in <Data>. Looking for EstimatedDataSize ≈ 500 is the wrong check.

What proves the cap: Nine still requests TruncationSize=51200. Under that client limit alone, bodies smaller than 50 KB would sync complete. In this log, 418 bodies have Truncated=1 with 500 < EstimatedDataSize < 51200 — Nine would not have truncated those by itself. The 15 complete bodies are all ≤ 463 bytes (under the 500-byte cap). Sync HTTP responses stay ~15–23 KB per batch, which also fits heavily truncated bodies.

Nine still offers “load the rest” (like iOS)

Truncation on Sync is not permanent for Nine. As in the earlier Nine run: open the message → button at the bottom → ItemOperations:Fetch with Store=Mailbox and no TruncationSize → full body. That path is intentionally not capped by maximumtruncationsize. So with a server-side Sync ceiling, Nine users keep a working repair path; mail only stays “crippled” if the remainder is never requested.

(This capture is Sync-only — no ItemOperations — so it doesn’t re-show the button; the mechanism itself is unchanged by the 500-byte Sync cap.)

9223372036854775807

That is the known bogus IMAP BINARY.SIZEPHP_INT_MAX issue (31 bodies in this log). It lives in #98, which is not part of this branch. On feat/maximum-truncation-size alone you will still see it until #98 is merged (or installed alongside).

Implications for keep vs drop

This is the first clear signal that a server-side Sync truncation ceiling is worth having upstream:

Client Default Sync truncation Re-fetches truncated body? Effect of a server Sync cap
iOS Mail already small yes (on open) safe; redundant for completeness
Nine 50 KB yes (button → ItemOperations) safe; Sync can be made smaller, full body still available on demand
Gmail Android 200 KB no unsafe for completeness (permanent clip)

So: iOS is smart out of the box; Nine stays smart after server-side truncation because it can fetch the rest; Gmail is not smart about truncation / large bodies at all. That makes the knob a real bandwidth control for well-behaved clients, with Gmail as the documented exception (leave at 0 for Gmail-heavy fleets, or accept clipped bodies). Still happy to wait on @ralflang for the final keep/drop call on this PR.

@SebastianArcus

Copy link
Copy Markdown

Thank you for analysing the logs and the detailed reply. It's good news that server enforced truncation works correctly in Nine as well. I confirm the log captured on purpose just the sync run - I avoided opening any messages during the capture just to keep it all clear and avoid mixing things up in the log.

I also agree that this feature looks useful and it would be worth merging it in.

Just a side note that I have submitted a bug report through the Gmail in-app feedback regarding Gmail's inability to cope with the server sending truncated messages - even when that truncation was requested by Gmail itself. No idea if it will get picked up by the dev team - but I thought it worth a try.

@ralflang

Copy link
Copy Markdown
Member

@TDannhauer is this ready for review?

@TDannhauer

TDannhauer commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Yes it is.

Strategically asked:
3 major mail client apps.

  1. Gmail app does not offer later loading of truncated mails, therefore truncation provides sync speed benefit, but without later full fetch capabilities is does not add user benefit.
  2. nine mail does offer later full fetch of truncated emails properly and by default has like Gmail way too large default sizes - and thus benefits fully from this PR.
  3. iOS does not need this PR at all as it has sensible truncation values per default - and offers later loading of truncated emails

Do you vote for merge or discard? If just one mail app benefits? For Gmail we filed an issue with all the details, maybe they improve it and Gmail benefits as well soon.

@SebastianArcus

SebastianArcus commented Aug 27, 2026

Copy link
Copy Markdown

Could I add a slight spanner in the works if that's ok. Could this be merged with the other feature - which implements minimum truncation size - to simplify the configuration? Possibly rename it to "force-truncation-size" - or similar - which means that if enabled, the server forces truncation size in all circumstances - both when client asks for a lower value and when the client asks for a higher value than the server config. So it could benefit the other scenario as well - where truncation could be set to a really large value on the server side - to make sure non-compliant clients - such as Gmail - download messages in full always and don't truncate them. I can't really think of a scenario where it would be beneficial to have separately two different settings at the server end - both a minimum and a maximum truncation size, and let clients choose a value in between.

I realise this has already been worked on for a while - sorry to barge in so late in the process.

@ralflang

Copy link
Copy Markdown
Member

It is welcome feedback. We should be careful not to complicate matters for the installed base but I think it's worth having both behind one switch. I do the same with the upcoming strategy switch in IMP (see latest ralf-lang.de post on this).

@ralflang
ralflang self-requested a review August 29, 2026 19:01
@ralflang
ralflang marked this pull request as ready for review August 29, 2026 19:02
@TDannhauer
TDannhauer force-pushed the feat/maximum-truncation-size branch from 624c4fe to 4d33f22 Compare August 31, 2026 17:32
@TDannhauer

Copy link
Copy Markdown
Contributor Author

rebased to latest FW_6_0

Replace maximum-only cap with forcetruncationsize: when set, the server
always uses that byte value for Sync body export whether the client
asked for more or less. ItemOperations Fetch stays uncapped. Still
reads deprecated maximumtruncationsize from existing configs.
@TDannhauer
TDannhauer force-pushed the feat/maximum-truncation-size branch from 4d33f22 to 362f323 Compare September 1, 2026 08:27
@TDannhauer TDannhauer changed the title feat(activesync): add optional Sync TruncationSize ceiling feat(activesync): force Sync TruncationSize override Sep 1, 2026
@TDannhauer

Copy link
Copy Markdown
Contributor Author

Could I add a slight spanner in the works if that's ok. Could this be merged with the other feature - which implements minimum truncation size - to simplify the configuration? Possibly rename it to "force-truncation-size" - or similar - which means that if enabled, the server forces truncation size in all circumstances - both when client asks for a lower value and when the client asks for a higher value than the server config. So it could benefit the other scenario as well - where truncation could be set to a really large value on the server side - to make sure non-compliant clients - such as Gmail - download messages in full always and don't truncate them. I can't really think of a scenario where it would be beneficial to have separately two different settings at the server end - both a minimum and a maximum truncation size, and let clients choose a value in between.

I realise this has already been worked on for a while - sorry to barge in so late in the process.

Thanks for your input!

I agree to your proposal: Any kind of truncation requires a working "fetch full later", then is is arbitrary at which size it is truncated.

Your proposal would simplify the required mind model of the responsible administrators: either rely completely on the client or define the truncation size server side. That's easier to understand than the "window size approach" where a client can freely decide itself, but just inside the window of minimum and maximum. I'll update the PR.

@SebastianArcus

Copy link
Copy Markdown

That's brilliant - thank you for the change. It will make the setting easier to use and understand by admins I think. Even I was struggling after a while to remember how the minimum and maximum works and which clients are affected under which circumstances. Much appreciated!

@TDannhauer

Copy link
Copy Markdown
Contributor Author

Please test the PR branch with Gmail and nine, I'll do with iOS, then we are ready for merge

@SebastianArcus

Copy link
Copy Markdown

Hmm - I can't get the forcetruncationsize option to show in the UI. I updated horde/activesync using the force-truncation-size PR, and the UI only showed "max_truncation_size" option. Then I also upgraded horde/horde - which now is at 6.2.1 - and now I don't have any options relating to truncation in the UI. Which version of which packages should I install (and how) to be able to test this PR please.

@TDannhauer

Copy link
Copy Markdown
Contributor Author

You need to update horde/base also, see horde/base#151

@SebastianArcus

Copy link
Copy Markdown

Hmm, not sure if I'm doing something wrong here:

$ sudo -u lighttpd composer require horde/base
Package "horde/base" does not exist but is provided by 2 packages. Which version constraint would you like to use? [*] *
./composer.json has been updated
Running composer update horde/base
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Writing lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Package pear/console_table is abandoned, you should avoid using it. No replacement was suggested.
Generating autoload files
Force mode not enabled, skipping removal of existing files
Applying /presets for absent files in /var/config
Distributing config .dist files from vendor to /var/config
  Distributed content/routes.php as .dist
  Distributed horde/prefs.php as .dist
  Distributed horde/routes.php as .dist
  Distributed horde/mime_drivers.php as .dist
  Distributed horde/conf.php.dist
  Distributed horde/registry.php as .dist
  Distributed imp/backends.php as .dist
  Distributed imp/prefs.php as .dist
  Distributed imp/routes.php as .dist
  Distributed imp/mime_drivers.php as .dist
  Distributed imp/conf.php.dist
  Distributed kronolith/prefs.php as .dist
  Distributed kronolith/routes.php as .dist
  Distributed kronolith/conf.php.dist
  Distributed timeobjects/routes.php as .dist
  Distributed turba/backends.php as .dist
  Distributed turba/attributes.php as .dist
  Distributed turba/prefs.php as .dist
  Distributed turba/routes.php as .dist
  Distributed turba/mime_drivers.php as .dist
  Distributed turba/conf.php.dist
Looking for registry snippets from apps
Configuration mode: proxy
Writing app configs to /var/config dir
Linking app configs to /vendor/horde/ Dir
Linking javascript tree to /web/js
Linking vendor assets to /web/js
Linking themes tree to /web/themes
Cleaning up obsolete web files
Cleaning up dead symlinks in web/js/ and web/themes/
    Skipped installation of bin bin/ci-bootstrap.sh for package horde/hordeymlfile: file not found in package
3 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
No security vulnerability advisories found.

@SebastianArcus

SebastianArcus commented Sep 3, 2026

Copy link
Copy Markdown

Ok - after much handholding by ChatGPT, it looks like force-truncation-size is a PR against horde/horde for some reason. Also I had to use an alias to satisfy the other dependencies. I now finally have [forcetruncationsize] in the UI and I will proceed to run the tests.

@SebastianArcus

Copy link
Copy Markdown

I've just sent the logs for Gmail over email - please ignore version "A" as I've omitted the Gmail logs. Also just a note that I've updated Gmail this evening to version 2026.08.17.974752392.Release

  1. For version B - I've used forcetruncationsize=10000. As far as I can tell, this doesn't appear to have had any effect on the Gmail app behaviour. I've sent 2 test emails - one with a large picture inline, the other one with over 400KB of text. The first email didn't have the picture ready downloaded - I had to wait for it to download after opening the email. The text email still truncated the text and didn't give any option to download the rest.

  2. For version C of the logs, I used forcetruncationsize=50. This behaved pretty much the same as above.

Can you work out from the Gmail logs if Gmail simply closes the connection and doesn't accept the entire email the server is sending? I expected that by using a large forcetruncationsize, I would be able to force Gmail to download the entire large text email and display it.

@TDannhauer

TDannhauer commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the B/C logs.

What went wrong

In both B and C the server still used Gmail’s own TruncationSize=200000. Force was not applied.

Proof: Sync HTTP response size was ~200 KB for the large mails. With force 10000 it would be ~10 KB; with force 50 it would be ~1–2 KB.

Most likely cause (given your earlier install path): the UI showed [forcetruncationsize], but conf.php still had no live value for that key. Changing the admin form is not enough — you must open ActiveSync configuration and click Save so Horde writes:

$conf['activesync']['sync']['forcetruncationsize'] = …

into the real conf.php. Until that save happens, PHP keeps honouring the client preference (or the old maximumtruncationsize key only).

Also: 10000 / 50 are below Gmail’s 200000 request. Even with force live they would truncate more, not deliver a 400 KB body. To push Gmail past 200000 you need a value larger than the message (e.g. 1048576).

Gmail did not close the connection early — Sync finished Status 1. It just never re-fetches truncated bodies. The inline-image wait is a separate ItemOperations attachment fetch (~15 MB), unrelated to truncation.

Exact packages / branches to install

You need both PR branches:

Composer package GitHub Branch PR
horde/activesync horde/ActiveSync feat/maximum-truncation-size #96
horde/horde horde/base (repo name ≠ Composer name) feat/force-truncation-size #151

Example (adjust VCS / alias as you already do for other dev-* packages):

composer require \
  'horde/activesync:dev-feat/maximum-truncation-size' \
  'horde/horde:dev-feat/force-truncation-size'

Then in the Horde admin UI: open ActiveSync config → set forcetruncationsizeSave → confirm the key exists in conf.php.

Sanity check that the library has the force API:

grep -n forceTruncationSize vendor/horde/activesync/lib/Horde/ActiveSync.php

Retest

  1. Prove force is live: set forcetruncationsize=500, Save, send a mail > 500 bytes. Sync HTTP for that Add must be ~1–2 KB (not ~200 KB).
  2. Gmail “full body on Sync”: set forcetruncationsize=1048576, Save, send a ~250–400 KB text mail. Expect Sync without Truncated=1. Force only changes Sync bytes; it cannot add a Gmail “download rest” button.

Happy to look at another log pair after that.

@SebastianArcus

SebastianArcus commented Sep 4, 2026

Copy link
Copy Markdown

Thank you for the detailed reply and for spending time on checking the logs. Sorry for getting muddled up between kilobytes and bytes - there were lots of zeroes floating around!

Please ignore log set D - after reading further into your comment I realised I needed to change the ActiveSync version. I confirm the setting $conf['activesync']['sync']['forcetruncationsize'] was already correct in the config file - but the ActiveSync version was wrong. I previously tried to update ActiveSync to the same PR branch as horde/horde - dev-feat/force-truncation-size (which failed) - I just didn't realised it is supposed to be under a different PR branch.

Log set E was run with forcetruncationsize set at 100 megabytes (hopefully). I confirm it downloaded and displayed in full in Gmail the email containing over 400 kilobytes of text body.

I also confirm (not included in the log) that sending an email with a 20 MB photo inserted inline works pretty much as expected:

  1. Initially the image placeholder is empty in the body of the email - even if you click on "Show pictures" at the top
  2. The attachments section at the bottom flashes rapidly for a while, during which time the attachment or inline image is downloading.
  3. Eventually the picture shows up in the attachments section.
  4. If you click on "Show pictures" at the top, it now shows as expected in the body of the email, instead of the initial placeholder

Coming back to "forcetruncationsize", is there any chance of having a third option please:

  1. 0 to disable forcetruncationsize and obey client preference
  2. actual number to peg the truncation size to a specific value
  3. a third option - like 1 or -1 - to always send the full body to the client. This would save having to guess the size of the largest email, or just put in really high values to catch any email sizes in the future.

It's just something which would make the feature easier and cleaner to use - but it isn't critical. It is always possible to put in something like 100 000 000 like I did - which should cover any email sizes.

@TDannhauer

Copy link
Copy Markdown
Contributor Author

Thanks — Log E confirms the override is working end-to-end.

Log E

Gmail still requested TruncationSize=200000, but with your large forcetruncationsize the Sync Add for Test 9 shipped the full body:

  • EstimatedDataSize=252381
  • no Truncated element
  • Sync HTTP response ~253 KB (full message, not capped at 200 KB)

That matches “downloaded and displayed in full in Gmail”. Earlier B/C failed because ActiveSync was on the wrong branch; with horde/activesync on feat/maximum-truncation-size and the key saved in conf.php, the feature does what we intended.

The inline-image behaviour you described (placeholder → attachment area busy → then “Show pictures”) is normal: Sync only announces the part; the ~MB download is a later ItemOperations Fetch, unrelated to body truncation.

“Always send full body” / infinite option

We are not adding a third sentinel (-1 / “never truncate”) for this PR.

Reasons:

  1. A large positive byte value already gives the Gmail behaviour you want (as in Log E).
  2. A true “unlimited” Sync ceiling is a footgun — one huge HTML-only message can hurt memory, Sync duration, or flaky clients. A high but finite cap is safer.
  3. Same admin model as maximumwindowsize: 0 = honor client, N > 0 = force that value. Keeps the UI and docs simple.

How to configure it

Goal forcetruncationsize
Honor client (default) 0
Cap Sync bodies (Nine / bandwidth) e.g. 50032768
Prefer full Sync bodies for Gmail-like clients a large byte value above typical text bodies

Examples for the Gmail-style case:

  • 1048576 (1 MiB) — enough for most large text mails
  • 10485760 (10 MiB) or 100000000 (what you used) — if you know you have bigger text bodies

There is no need to guess “the largest email ever”; pick a ceiling you are willing to put on a single Sync export. Attachments / inline images stay on ItemOperations and are not limited by this setting.

Thanks again for the careful retests — this unblocks merge from the Gmail side once Nine/iOS checks are done.

@TDannhauer

Copy link
Copy Markdown
Contributor Author

ready to merge @ralflang

@SebastianArcus

Copy link
Copy Markdown

I've just sent over logs 'F' which are for the Nine sync using 500 bytes as `forcetruncationsize'. Everything looks normal and Nine behaves as expected - the message is initially truncated, then clicking the button at the bottom of the window then loads the rest of the message and attachments. Probably not strictly needed - as Nine was already behaving properly in previous tests - but I thought I'd send them over for the sake of completeness.

Thank you for all your work on this new feature. I think it will be a really useful capability on future Horde deployments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants